﻿package com.janisRadins.asDraw {
	import flash.display.GraphicsPath;
	import flash.events.EventDispatcher;
	import flash.geom.Point;
	/**
	 * Event dispathed as any of sub curve properties have been changed.<br>
	 * This event is cancellable. Calling <code>preventDefault()</code> on event will cancel data change.
	 * @eventType com.janisRadins.asDraw.CurveChangeEvent.CHANGE
	 */
	[Event(name = "change", type = "com.janisRadins.asDraw.CurveChangeEvent")]
	/**
	 * Basic building block for any vector sequence which incorporates basics of all system 
	 * functionality
	 * @author Jānis Radiņš
	 */
	public class Curve extends EventDispatcher implements ICurve {
		/**
		 * Setting how many curve length segments should be calulated in order to calculate
		 * curve length
		 */
		protected const CURVE_LENGTH_PRECISION:Number = 0.05;
		/**
		 * Constant which defines how far the other point for angle calculation should be calculated for angle on Curve method
		 */
		protected const CURVE_ANGLE_PRECISION:Number = 0.001;
		/** 
		 * Variable which keeps control point value internatly
		 * @private 
		 */
		protected var controlPoint:Point = null;
		/**
		 * Variable which keeps value for visual peak of curve
		 * @private 
		 */
		protected var visualControlPoint:Point = null;
		/**
		 * Variable which keeps anchor point value internatly
		 * @private
		 */
		protected var anchorPoint:Point = null;
		/**
		 * Curve length property
		 * @private
		 **/
		protected var _length:Number = NaN;
		/**
		 * Inner holder for is straight property
		 * @private
		 */
		protected var _isStraight:Boolean = false;
		/**
		 * Length distribution array left after length calculation. Needed to adjust length to position on bezzier.
		 * @private
		 */
		protected var lengthDistribution:Array = null;
		/**
		 * Constructor of Curve.
		 * Each Curve is abstracter from all other curves therefore each Curve thinks it's starting 
		 * position is at [0, 0]
		 * Therefor this class should receive only values relative to previouse Curve anchor point.
		 * Straight lines control point should be set to same value as anchor point. Anyhow this class checks if 
		 * control point is on same line.
		 * Pass NaN for controlX and controlY if you want control point to be automatically set to same values as
		 * anchor point.
		 * @param	controlX	Controll point X value
		 * @param	controlY	Controll point Y value
		 * @param	anchorX		Anchor point X value
		 * @param	anchorY		Anchor point Y value
		 */
		public function Curve(controlX:Number = NaN, controlY:Number = NaN, anchorX:Number = 0, anchorY:Number = 0) {
			anchorPoint = new Point(anchorX, anchorY);
			if (isNaN(controlX) || isNaN(controlY)) {
				controlPoint = new Point(anchorX, anchorY);
			} else {
				controlPoint = new Point(controlX, controlY);
			}
		}
		/**
		 * Inner function that calulates position on Curve for any of dymansions.
		 * Used for inner calculations
		 * @param	ratio	Requested position on curve in range 0 - 1
		 * @param	p1		Curve start position
		 * @param	p2		Curve control position
		 * @param	p3		Curve anchor position
		 * @return calculated value
		 * @private
		 */
		protected function getPositionOnCurve(ratio:Number, p1:Number, p2:Number, p3:Number):Number {
			return p1 + ratio * (2 * (1 - ratio) * (p2 - p1) + ratio * (p3 - p1));
		}
		/**
		 * Return Control point value by point on curve
		 * @param	ratio	Position at which point on curve resists
		 * @param	startP	Curve start position
		 * @param	pT		Point on curve value
		 * @param	endP	Curve anchor position
		 * @return
		 */
		protected function getCrtlPointByPointOnCurve(ratio:Number, startP:Number, pT:Number, endP:Number):Number {
			return ((pT - startP) / ratio - ratio * (endP - startP)) / (2 * (1 - ratio)) + startP;
		}
		/**
		 * Inner function that calculates anchor point position for any of dymansions
		 * @param	startRatio	Start position in range 0 - 1
		 * @param	endRatio	End position in range 0 - 1
		 * @param	p1		Curve start position
		 * @param	p2		Curve control position
		 * @param	p3		Curve anchor position
		 * @return calculated value
		 * @private
		 */
		protected function getControlPosition(startRatio:Number, endRatio:Number, p1:Number, p2:Number, p3:Number):Number {
			return p1 + (startRatio + endRatio) * (p2 - p1) + startRatio * endRatio * (p3 - 2 * p2 + p1);
		}
		/**
		 * Method which is executed as any of point properies change and determines if this is straight line and 
		 * calulates Curve length
		 */
		protected function updateValues():void {
			lengthDistribution = new Array();
			_isStraight = (controlPoint.equals(anchorPoint) || controlPoint.x/controlPoint.y == anchorPoint.x/anchorPoint.y);
			if (_isStraight == true) {
				_length = Math.sqrt(Math.pow(anchorPoint.x, 2) + Math.pow(anchorPoint.y, 2));
			} else {
				_length = 0;
				var point1:Point = new Point();
				var point2:Point = null;
				_length = 0;
				for (var i:Number = 0; i < 1; i += CURVE_LENGTH_PRECISION) {
					point2 = getPointAtPosition(i + CURVE_LENGTH_PRECISION);
					var segData:Array = new Array(i, _length);
					_length += Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
					segData.push(_length);
					lengthDistribution.push(segData);
					point1 = point2;
				}
			}
		}
		/**
		 * Method that converts length value to closest match to Curve ratio
		 * @param	len		Value in range 0 - length
		 * @return Number
		 */
		protected function lengthToRatio(len:Number):Number {
			var ratio:Number = 0;
			if (isStraight) {
				ratio = len / length;
			} else {
				for each (var segment:Array in lengthDistribution) {
					if (len >= segment[1] && len <= segment[2]) {
						ratio = segment[0] + ((len - segment[1]) / (segment[2] - segment[1])) * CURVE_LENGTH_PRECISION;
					}
				}
			}
			// CURVE_LENGTH_PRECISION or floating point calculations make ratio a bit bigger when len == length
			ratio = Math.min(ratio, 1);
			return ratio;
		}
		/**
		 * Method that returns point value on Curve for position requested
		 * @param	position	Value in range 0 - 1 which defines which position should be calculated
		 * @return Point with x and y coordinate of point
		 */
		public function getPointAtPosition(position:Number):Point {
			switch(true) {
				case position < 0	:	position = 0;	break;
				case position > 1	:	position = 1;	break;
			}
			if (isStraight) {
				var angle:Number = Math.atan2(controlPoint.x, controlPoint.y);
				return new Point(Math.sin(angle) * (length * position), Math.cos(angle) * (length * position));
			}
			return new Point(
				getPositionOnCurve(position, 0, controlPoint.x, anchorPoint.x),
				getPositionOnCurve(position, 0, controlPoint.y, anchorPoint.y)
			);
		}
		/**
		 * Method that returns point on Curve by length value;
		 * @param	position
		 * @return	Point with x and y coordinate of point
		 */
		public function getPointOnCurve(position:Number):Point {
			return getPointAtPosition(lengthToRatio(position));
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurve#getAngleAtPoint()
		 */
		public function getAngleAtPoint(position:Number):Number {
			var pointRatio:Number = Math.min(lengthToRatio(position), 1 - CURVE_ANGLE_PRECISION);
			var point:Point = getPointAtPosition(pointRatio + 0.001).subtract(getPointAtPosition(pointRatio));
			return Math.atan2(point.y, point.x);
		}
		/**
		 * Method that returns GraphicsPath defining curve segment
		 * @param	start	Start position of sub curve. Value must be in range 0 - 1
		 * @param	end		End position of sub curve. Value must be in range 0 - 1
		 * @return A GraphicsPath which defines sub curve
		 */
		public function getSegmentByRatio(start:Number, end:Number):GraphicsPath {
			var path:GraphicsPath = new GraphicsPath();
			var moveTo:Point = getPointAtPosition(start);
			var endPoint:Point = getPointAtPosition(end);
			path.moveTo(moveTo.x, moveTo.y);
			if (isStraight) {
				path.wideLineTo(endPoint.x, endPoint.y);
			} else {
				var ctrlPoint:Point = new Point(
					getControlPosition(start, end, 0, controlPoint.x, anchorPoint.x),
					getControlPosition(start, end, 0, controlPoint.y, anchorPoint.y)
				);
				path.curveTo(ctrlPoint.x, ctrlPoint.y, endPoint.x, endPoint.y);	
			}
			return path;
		}
		/**
		 * Method that returns curve segment by length coordinates
		 * @param	start	Start position of sub curve in range 0 - length
		 * @param	end		End position of sub curve in range 0 - length
		 * @return	A GraphicsPath which defines sub curve
		 */
		public function getCurveSegment(start:Number = NaN, end:Number = NaN):GraphicsPath {
			if (isNaN(start) || isNaN(end)) {
				return getSegmentByRatio(0, 1);
			}
			if (isStraight) {
				return getSegmentByRatio(start / length, end / length);
			}
			return getSegmentByRatio(lengthToRatio(start), lengthToRatio(end));
		}
		/**
		 * Curve control point X position.
		 */
		public function get controlX():Number {
			return controlPoint.x;
		}
		public function set controlX(value:Number):void {
			if (value == controlPoint.x) {
				return;
			}
			if (dispatchEvent(new CurveChangeEvent(CurveChangeEvent.CHANGE, CurveChangeEvent.CONTROL_X, controlPoint.x, value)) == true) {
				controlPoint.x = value;
				_length = NaN;
			}
		}
		/**
		 * Curve control point Y position.
		 */
		public function get controlY():Number {
			return controlPoint.y;
		}
		public function set controlY(value:Number):void {
			if (value == controlPoint.y) {
				return;
			}
			if (dispatchEvent(new CurveChangeEvent(CurveChangeEvent.CHANGE, CurveChangeEvent.CONTROL_y, controlPoint.y, value)) == true) {
				controlPoint.y = value;
				_length = NaN;
				visualControlPoint = null;
			}
		}
		/**
		 * Curve anchor point X position.
		 */
		public function get anchorX():Number {
			return anchorPoint.x;
		}
		public function set anchorX(value:Number):void {
			if (value == anchorPoint.x) {
				return;
			}
			if (dispatchEvent(new CurveChangeEvent(CurveChangeEvent.CHANGE, CurveChangeEvent.ANCHOR_X, anchorPoint.x, value)) == true) {
				anchorPoint.x = value;
				_length = NaN;
				visualControlPoint = null;
			}
		}
		/**
		 * Curve anchor point Y position.
		 */
		public function get anchorY():Number {
			return anchorPoint.y;
		}
		public function set anchorY(value:Number):void {
			if (value == anchorPoint.y) {
				return;
			}
			if (dispatchEvent(new CurveChangeEvent(CurveChangeEvent.CHANGE, CurveChangeEvent.ANCHOR_Y, anchorPoint.y, value)) == true) {
				anchorPoint.y = value;
				_length = NaN;
			}
		}
		
		protected function updateVisualControlPoint():void {
			visualControlPoint = getPointAtPosition(0.5);
		}
		/**
		 * Value of curve visual peak X
		 */
		public function get visualControlX():Number {
			if (visualControlPoint == null) {
				updateVisualControlPoint();
			}
			return visualControlPoint.x;
		}
		public function set visualControlX(value:Number):void {
			if (visualControlX == value) {
				return;
			}
			controlX = getCrtlPointByPointOnCurve(0.5, 0, value, anchorPoint.x);
		}
		/**
		 * Value of curve visual peak Y
		 */
		public function get visualControlY():Number {
			if (visualControlPoint == null) {
				updateVisualControlPoint();
			}
			return visualControlPoint.y;
		}
		public function set visualControlY(value:Number):void {
			if (visualControlY == value) {
				return;
			}
			controlY = getCrtlPointByPointOnCurve(0.5, 0, value, anchorPoint.y);
		}
		
		/**
		 * Curve length in pixels
		 */
		public function get length():Number {
			if (isNaN(_length)) {
				updateValues();
			}
			return _length;
		}
		/**
		 * Flag which identify whether Curve is straight line (true) or bended (false)
		 */
		public function get isStraight():Boolean {
			if (isNaN(_length)) {
				updateValues();
			}
			return _isStraight;
		}
		/**
		 * Creates exact clone of current Curve instance
		 * @return Curve
		 */
		public function clone():Curve {
			return new Curve(controlX, controlY, anchorX, anchorY);
		}
		/**
		 * Class toStirng implementation to be witin trace or any other sting representation
		 * @return String
		 */
		override public function toString():String {
			return "Curve [controlX:" + controlX + ", controlY:" + controlY + ", anchorX:" + anchorX + ", anchorY: " + anchorY + "]";
		}
	}
}